// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE

(function(mod) {
  if (typeof exports == 'object' && typeof module == 'object')
    // CommonJS
    mod(require('../../lib/codemirror'));
  else if (typeof define == 'function' && define.amd)
    // AMD
    define(['../../lib/codemirror'], mod);
  // Plain browser env
  else mod(CodeMirror);
})(function(CodeMirror) {
  'use strict';

  CodeMirror.defineMode('vb', function(conf, parserConf) {
    var ERRORCLASS = 'error';

    function wordRegexp(words) {
      return new RegExp('^((' + words.join(')|(') + '))\\b', 'i');
    }

    var singleOperators = new RegExp('^[\\+\\-\\*/%&\\\\|\\^~<>!]');
    var singleDelimiters = new RegExp('^[\\(\\)\\[\\]\\{\\}@,:`=;\\.]');
    var doubleOperators = new RegExp(
      '^((==)|(<>)|(<=)|(>=)|(<>)|(<<)|(>>)|(//)|(\\*\\*))',
    );
    var doubleDelimiters = new RegExp(
      '^((\\+=)|(\\-=)|(\\*=)|(%=)|(/=)|(&=)|(\\|=)|(\\^=))',
    );
    var tripleDelimiters = new RegExp('^((//=)|(>>=)|(<<=)|(\\*\\*=))');
    var identifiers = new RegExp('^[_A-Za-z][_A-Za-z0-9]*');

    var openingKeywords = [
      'class',
      'module',
      'sub',
      'enum',
      'select',
      'while',
      'if',
      'function',
      'get',
      'set',
      'property',
      'try',
    ];
    var middleKeywords = ['else', 'elseif', 'case', 'catch'];
    var endKeywords = ['next', 'loop'];

    var operatorKeywords = ['and', 'or', 'not', 'xor', 'in'];
    var wordOperators = wordRegexp(operatorKeywords);
    var commonKeywords = [
      'as',
      'dim',
      'break',
      'continue',
      'optional',
      'then',
      'until',
      'goto',
      'byval',
      'byref',
      'new',
      'handles',
      'property',
      'return',
      'const',
      'private',
      'protected',
      'friend',
      'public',
      'shared',
      'static',
      'true',
      'false',
    ];
    var commontypes = [
      'integer',
      'string',
      'double',
      'decimal',
      'boolean',
      'short',
      'char',
      'float',
      'single',
    ];

    var keywords = wordRegexp(commonKeywords);
    var types = wordRegexp(commontypes);
    var stringPrefixes = '"';

    var opening = wordRegexp(openingKeywords);
    var middle = wordRegexp(middleKeywords);
    var closing = wordRegexp(endKeywords);
    var doubleClosing = wordRegexp(['end']);
    var doOpening = wordRegexp(['do']);

    var indentInfo = null;

    CodeMirror.registerHelper(
      'hintWords',
      'vb',
      openingKeywords
        .concat(middleKeywords)
        .concat(endKeywords)
        .concat(operatorKeywords)
        .concat(commonKeywords)
        .concat(commontypes),
    );

    function indent(_stream, state) {
      state.currentIndent++;
    }

    function dedent(_stream, state) {
      state.currentIndent--;
    }
    // tokenizers
    function tokenBase(stream, state) {
      if (stream.eatSpace()) {
        return null;
      }

      var ch = stream.peek();

      // Handle Comments
      if (ch === "'") {
        stream.skipToEnd();
        return 'comment';
      }

      // Handle Number Literals
      if (stream.match(/^((&H)|(&O))?[0-9\.a-f]/i, false)) {
        var floatLiteral = false;
        // Floats
        if (stream.match(/^\d*\.\d+F?/i)) {
          floatLiteral = true;
        } else if (stream.match(/^\d+\.\d*F?/)) {
          floatLiteral = true;
        } else if (stream.match(/^\.\d+F?/)) {
          floatLiteral = true;
        }

        if (floatLiteral) {
          // Float literals may be "imaginary"
          stream.eat(/J/i);
          return 'number';
        }
        // Integers
        var intLiteral = false;
        // Hex
        if (stream.match(/^&H[0-9a-f]+/i)) {
          intLiteral = true;
        }
        // Octal
        else if (stream.match(/^&O[0-7]+/i)) {
          intLiteral = true;
        }
        // Decimal
        else if (stream.match(/^[1-9]\d*F?/)) {
          // Decimal literals may be "imaginary"
          stream.eat(/J/i);
          // TODO - Can you have imaginary longs?
          intLiteral = true;
        }
        // Zero by itself with no other piece of number.
        else if (stream.match(/^0(?![\dx])/i)) {
          intLiteral = true;
        }
        if (intLiteral) {
          // Integer literals may be "long"
          stream.eat(/L/i);
          return 'number';
        }
      }

      // Handle Strings
      if (stream.match(stringPrefixes)) {
        state.tokenize = tokenStringFactory(stream.current());
        return state.tokenize(stream, state);
      }

      // Handle operators and Delimiters
      if (stream.match(tripleDelimiters) || stream.match(doubleDelimiters)) {
        return null;
      }
      if (
        stream.match(doubleOperators) ||
        stream.match(singleOperators) ||
        stream.match(wordOperators)
      ) {
        return 'operator';
      }
      if (stream.match(singleDelimiters)) {
        return null;
      }
      if (stream.match(doOpening)) {
        indent(stream, state);
        state.doInCurrentLine = true;
        return 'keyword';
      }
      if (stream.match(opening)) {
        if (!state.doInCurrentLine) indent(stream, state);
        else state.doInCurrentLine = false;
        return 'keyword';
      }
      if (stream.match(middle)) {
        return 'keyword';
      }

      if (stream.match(doubleClosing)) {
        dedent(stream, state);
        dedent(stream, state);
        return 'keyword';
      }
      if (stream.match(closing)) {
        dedent(stream, state);
        return 'keyword';
      }

      if (stream.match(types)) {
        return 'keyword';
      }

      if (stream.match(keywords)) {
        return 'keyword';
      }

      if (stream.match(identifiers)) {
        return 'variable';
      }

      // Handle non-detected items
      stream.next();
      return ERRORCLASS;
    }

    function tokenStringFactory(delimiter) {
      var singleline = delimiter.length == 1;
      var OUTCLASS = 'string';

      return function(stream, state) {
        while (!stream.eol()) {
          stream.eatWhile(/[^'"]/);
          if (stream.match(delimiter)) {
            state.tokenize = tokenBase;
            return OUTCLASS;
          } else {
            stream.eat(/['"]/);
          }
        }
        if (singleline) {
          if (parserConf.singleLineStringErrors) {
            return ERRORCLASS;
          } else {
            state.tokenize = tokenBase;
          }
        }
        return OUTCLASS;
      };
    }

    function tokenLexer(stream, state) {
      var style = state.tokenize(stream, state);
      var current = stream.current();

      // Handle '.' connected identifiers
      if (current === '.') {
        style = state.tokenize(stream, state);
        if (style === 'variable') {
          return 'variable';
        } else {
          return ERRORCLASS;
        }
      }

      var delimiter_index = '[({'.indexOf(current);
      if (delimiter_index !== -1) {
        indent(stream, state);
      }
      if (indentInfo === 'dedent') {
        if (dedent(stream, state)) {
          return ERRORCLASS;
        }
      }
      delimiter_index = '])}'.indexOf(current);
      if (delimiter_index !== -1) {
        if (dedent(stream, state)) {
          return ERRORCLASS;
        }
      }

      return style;
    }

    var external = {
      electricChars: 'dDpPtTfFeE ',
      startState: function() {
        return {
          tokenize: tokenBase,
          lastToken: null,
          currentIndent: 0,
          nextLineIndent: 0,
          doInCurrentLine: false,
        };
      },

      token: function(stream, state) {
        if (stream.sol()) {
          state.currentIndent += state.nextLineIndent;
          state.nextLineIndent = 0;
          state.doInCurrentLine = 0;
        }
        var style = tokenLexer(stream, state);

        state.lastToken = { style: style, content: stream.current() };

        return style;
      },

      indent: function(state, textAfter) {
        var trueText = textAfter.replace(/^\s+|\s+$/g, '');
        if (
          trueText.match(closing) ||
          trueText.match(doubleClosing) ||
          trueText.match(middle)
        )
          return conf.indentUnit * (state.currentIndent - 1);
        if (state.currentIndent < 0) return 0;
        return state.currentIndent * conf.indentUnit;
      },

      lineComment: "'",
    };
    return external;
  });

  CodeMirror.defineMIME('text/x-vb', 'vb');
});
